package com.wy.repository;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;

import dream.flying.flower.framework.security.constant.ConstOAuthGrantType;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 基于redis的客户端repository实现
 *
 * @author 飞花梦影
 * @date 2024-11-07 09:33:19
 * @git {@link https://github.com/dreamFlyingFlower}
 */
@Slf4j
@Repository
@RequiredArgsConstructor
public class RedisRegisteredClientRepository implements RegisteredClientRepository {

	/**
	 * 提供给客户端初始化使用(不需要可删除)
	 */
	private final PasswordEncoder passwordEncoder;

	private final RedisClientRepository repository;

	private final static ObjectMapper MAPPER = new ObjectMapper();

	static {
		// 初始化序列化配置
		ClassLoader classLoader = RedisRegisteredClientRepository.class.getClassLoader();
		// 加载security提供的Modules
		List<Module> modules = SecurityJackson2Modules.getModules(classLoader);
		MAPPER.registerModules(modules);
		// 加载Authorization Server提供的Module
		MAPPER.registerModule(new OAuth2AuthorizationServerJackson2Module());
		// 将OAuth2TokenFormat序列化为字符串,防止本机镜像打包后反序列化失败
		/*
		 * SimpleModule module = new SimpleModule("OAuth2TokenFormatSerializer");
		 * module.addSerializer(OAuth2TokenFormat.class, new
		 * OAuth2TokenFormatSerializer()); MAPPER.registerModule(module);
		 */
	}

	@Override
	public void save(RegisteredClient registeredClient) {
		Assert.notNull(registeredClient, "registeredClient cannot be null");
		this.repository.findByClientId(registeredClient.getClientId())
				.ifPresent(existingRegisteredClient -> this.repository.deleteById(existingRegisteredClient.getId()));
		this.repository.save(toEntity(registeredClient));
	}

	@Override
	public RegisteredClient findById(String id) {
		Assert.hasText(id, "id cannot be empty");
		return this.repository.findById(id).map(this::toObject).orElse(null);
	}

	@Override
	public RegisteredClient findByClientId(String clientId) {
		Assert.hasText(clientId, "clientId cannot be empty");
		return this.repository.findByClientId(clientId).map(this::toObject).orElse(null);
	}

	private RegisteredClient toObject(RedisRegisteredClient client) {
		Set<String> clientAuthenticationMethods =
				StringUtils.commaDelimitedListToSet(client.getClientAuthenticationMethods());
		Set<String> authorizationGrantTypes = StringUtils.commaDelimitedListToSet(client.getAuthorizationGrantTypes());
		Set<String> redirectUris = StringUtils.commaDelimitedListToSet(client.getRedirectUris());
		Set<String> postLogoutRedirectUris = StringUtils.commaDelimitedListToSet(client.getPostLogoutRedirectUris());
		Set<String> clientScopes = StringUtils.commaDelimitedListToSet(client.getScopes());

		RegisteredClient.Builder builder = RegisteredClient.withId(client.getId())
				.clientId(client.getClientId())
				.clientIdIssuedAt(client.getClientIdIssuedAt())
				.clientSecret(client.getClientSecret())
				.clientSecretExpiresAt(client.getClientSecretExpiresAt())
				.clientName(client.getClientName())
				.clientAuthenticationMethods(authenticationMethods -> clientAuthenticationMethods
						.forEach(authenticationMethod -> authenticationMethods
								.add(resolveClientAuthenticationMethod(authenticationMethod))))
				.authorizationGrantTypes((grantTypes) -> authorizationGrantTypes
						.forEach(grantType -> grantTypes.add(resolveAuthorizationGrantType(grantType))))
				.redirectUris((uris) -> uris.addAll(redirectUris))
				.postLogoutRedirectUris((uris) -> uris.addAll(postLogoutRedirectUris))
				.scopes((scopes) -> scopes.addAll(clientScopes));

		Map<String, Object> clientSettingsMap = parseMap(client.getClientSettings());
		builder.clientSettings(ClientSettings.withSettings(clientSettingsMap).build());

		Map<String, Object> tokenSettingsMap = parseMap(client.getTokenSettings());
		builder.tokenSettings(TokenSettings.withSettings(tokenSettingsMap).build());

		return builder.build();
	}

	private RedisRegisteredClient toEntity(RegisteredClient registeredClient) {
		List<String> clientAuthenticationMethods =
				new ArrayList<>(registeredClient.getClientAuthenticationMethods().size());
		registeredClient.getClientAuthenticationMethods()
				.forEach(clientAuthenticationMethod -> clientAuthenticationMethods
						.add(clientAuthenticationMethod.getValue()));

		List<String> authorizationGrantTypes = new ArrayList<>(registeredClient.getAuthorizationGrantTypes().size());
		registeredClient.getAuthorizationGrantTypes()
				.forEach(authorizationGrantType -> authorizationGrantTypes.add(authorizationGrantType.getValue()));

		RedisRegisteredClient entity = new RedisRegisteredClient();
		entity.setId(registeredClient.getId());
		entity.setClientId(registeredClient.getClientId());
		entity.setClientIdIssuedAt(registeredClient.getClientIdIssuedAt());
		entity.setClientSecret(registeredClient.getClientSecret());
		entity.setClientSecretExpiresAt(registeredClient.getClientSecretExpiresAt());
		entity.setClientName(registeredClient.getClientName());
		entity.setClientAuthenticationMethods(
				StringUtils.collectionToCommaDelimitedString(clientAuthenticationMethods));
		entity.setAuthorizationGrantTypes(StringUtils.collectionToCommaDelimitedString(authorizationGrantTypes));
		entity.setRedirectUris(StringUtils.collectionToCommaDelimitedString(registeredClient.getRedirectUris()));
		entity.setPostLogoutRedirectUris(
				StringUtils.collectionToCommaDelimitedString(registeredClient.getPostLogoutRedirectUris()));
		entity.setScopes(StringUtils.collectionToCommaDelimitedString(registeredClient.getScopes()));
		entity.setClientSettings(writeMap(registeredClient.getClientSettings().getSettings()));
		entity.setTokenSettings(writeMap(registeredClient.getTokenSettings().getSettings()));

		return entity;
	}

	private Map<String, Object> parseMap(String data) {
		try {
			return MAPPER.readValue(data, new TypeReference<>() {
			});
		} catch (Exception ex) {
			log.info("转换失败的JSON为：{}", data);
			throw new IllegalArgumentException(ex.getMessage(), ex);
		}
	}

	private String writeMap(Map<String, Object> data) {
		try {
			return MAPPER.writeValueAsString(data);
		} catch (Exception ex) {
			throw new IllegalArgumentException(ex.getMessage(), ex);
		}
	}

	private static AuthorizationGrantType resolveAuthorizationGrantType(String authorizationGrantType) {
		if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(authorizationGrantType)) {
			return AuthorizationGrantType.AUTHORIZATION_CODE;
		} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(authorizationGrantType)) {
			return AuthorizationGrantType.CLIENT_CREDENTIALS;
		} else if (AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(authorizationGrantType)) {
			return AuthorizationGrantType.REFRESH_TOKEN;
		}
		// Custom authorization grant type
		return new AuthorizationGrantType(authorizationGrantType);
	}

	private static ClientAuthenticationMethod resolveClientAuthenticationMethod(String clientAuthenticationMethod) {
		if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue().equals(clientAuthenticationMethod)) {
			return ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
		} else if (ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue().equals(clientAuthenticationMethod)) {
			return ClientAuthenticationMethod.CLIENT_SECRET_POST;
		} else if (ClientAuthenticationMethod.NONE.getValue().equals(clientAuthenticationMethod)) {
			return ClientAuthenticationMethod.NONE;
		}
		// Custom client authentication method
		return new ClientAuthenticationMethod(clientAuthenticationMethod);
	}

	/**
	 * 容器启动后初始化客户端 (不需要可删除)
	 */
	@PostConstruct
	public void initClients() {
		log.info("Initialize client information to Redis.");
		// 默认需要授权确认
		ClientSettings.Builder builder = ClientSettings.builder().requireAuthorizationConsent(Boolean.TRUE);

		TokenSettings.Builder tokenSettingsBuilder = TokenSettings.builder()
				// 自包含token(jwt)
				.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
				// Access Token 存活时间：2小时
				.accessTokenTimeToLive(Duration.ofHours(2L))
				// 授权码存活时间：5分钟
				.authorizationCodeTimeToLive(Duration.ofMinutes(5L))
				// 设备码存活时间：5分钟
				.deviceCodeTimeToLive(Duration.ofMinutes(5L))
				// Refresh Token 存活时间：7天
				.refreshTokenTimeToLive(Duration.ofDays(7L))
				// 刷新 Access Token 后是否重用 Refresh Token
				.reuseRefreshTokens(Boolean.TRUE)
				// 设置 Id Token 加密方式
				.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256);

		// 正常授权码客户端
		RegisteredClient registeredClient = RegisteredClient.withId("messaging-client")
				// 客户端id
				.clientId("messaging-client")
				// 客户端名称
				.clientName("授权码")
				// 客户端秘钥,使用密码解析器加密
				.clientSecret(passwordEncoder.encode("123456"))
				// 客户端认证方式,基于请求头的认证
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				// 配置资源服务器使用该客户端获取授权时支持的方式
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
				.authorizationGrantType(ConstOAuthGrantType.SMS_CODE)
				// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
				.redirectUri("http://127.0.0.1:5173/OAuth2Redirect")
				.redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc")
				// 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
				.scope(OidcScopes.OPENID)
				.scope(OidcScopes.PROFILE)
				// 指定scope
				.scope("message.read")
				.scope("message.write")
				// 客户端设置,设置用户需要确认授权
				.clientSettings(builder.build())
				// token相关配置
				.tokenSettings(tokenSettingsBuilder.build())
				.build();

		// 匿名令牌客户端
		RegisteredClient opaqueClient = RegisteredClient.withId("opaque-client")
				// 客户端id
				.clientId("opaque-client")
				// 客户端名称
				.clientName("匿名token")
				// 客户端秘钥,使用密码解析器加密
				.clientSecret(passwordEncoder.encode("123456"))
				// 客户端认证方式,基于请求头的认证
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				// 配置资源服务器使用该客户端获取授权时支持的方式
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
				// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
				.redirectUri("http://127.0.0.1:5173/OAuth2Redirect")
				.redirectUri("https://flow-cloud.love/OAuth2Redirect")
				.redirectUri("https://j1zr8ren8w.51xd.pub/OAuth2Redirect")
				.redirectUri("https://j1zr8ren8w.51xd.pub/demonstrate/OAuth2Redirect")
				.redirectUri("https://authorization-example.vercel.app/OAuth2Redirect")
				// 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
				.scope(OidcScopes.OPENID)
				.scope(OidcScopes.PROFILE)
				// 指定scope
				.scope("message.read")
				.scope("message.write")
				// 客户端设置,设置用户需要确认授权
				.clientSettings(builder.build())
				// token相关配置, 设置token为匿名token(opaque token)
				.tokenSettings(tokenSettingsBuilder.accessTokenFormat(OAuth2TokenFormat.REFERENCE).build())
				.build();

		// 设备码授权客户端
		RegisteredClient deviceClient = RegisteredClient.withId("device-message-client")
				.clientId("device-message-client")
				.clientName("普通公共客户端")
				// 公共客户端
				.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
				// 设备码授权
				.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				// 指定scope
				.scope("message.read")
				.scope("message.write")
				// token相关配置
				.tokenSettings(tokenSettingsBuilder.build())
				.build();

		// PKCE客户端
		RegisteredClient pkceClient = RegisteredClient.withId("pkce-message-client")
				.clientId("pkce-message-client")
				.clientName("PKCE流程")
				// 公共客户端
				.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
				// 设备码授权
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				// 授权码模式回调地址,oauth2.1已改为精准匹配,不能只设置域名,并且屏蔽了localhost,本机使用127.0.0.1访问
				.redirectUri("http://127.0.0.1:5173/PkceRedirect")
				.redirectUri("https://j1zr8ren8w.51xd.pub/PkceRedirect")
				.redirectUri("https://j1zr8ren8w.51xd.pub/demonstrate/PkceRedirect")
				.redirectUri("https://flow-cloud.love/PkceRedirect")
				.redirectUri("https://authorization-example.vercel.app/PkceRedirect")
				// 开启 PKCE 流程
				.clientSettings(builder.requireProofKey(Boolean.TRUE).build())
				// 指定scope
				.scope("message.read")
				.scope("message.write")
				// token相关配置
				.tokenSettings(tokenSettingsBuilder.build())
				.build();

		RegisteredClient privateKeyJwtClient = RegisteredClient.withId(IdWorker.getIdStr())
				.clientId("private-key-jwt-client")
				.clientSecret("{noop}12345678")
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				.redirectUri("https://www.baidu.com")
				.redirectUri("http://127.0.0.1:5173/OAuth2Redirect")
				.redirectUri("http://127.0.0.1:8000/login/oauth2/code/private-key-client-oidc")
				.redirectUri("http://127.0.0.1:8080/swagger-ui/oauth2-redirect.html")
				.postLogoutRedirectUri("http://127.0.0.1:8080/")
				.scope(OidcScopes.OPENID)
				.scope(OidcScopes.PROFILE)
				.clientSettings(ClientSettings.builder()
						.requireAuthorizationConsent(true)
						.jwkSetUrl("http://127.0.0.1:8000/jwkSet")
						.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
						.build())
				.build();

		RegisteredClient privateKeyJwtClient1 = RegisteredClient.withId(IdWorker.getIdStr())
				.clientId("private-key-jwt-client1")
				.clientSecret("{noop}123456789")
				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
				.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
				.redirectUri("https://www.baidu.com")
				.redirectUri("http://127.0.0.1:5173/OAuth2Redirect")
				.redirectUri("http://127.0.0.1:7000/login/oauth2/code/private-key-client-oidc")
				.redirectUri("http://127.0.0.1:8080/swagger-ui/oauth2-redirect.html")
				.postLogoutRedirectUri("http://127.0.0.1:8080/")
				.scope(OidcScopes.OPENID)
				.scope(OidcScopes.PROFILE)
				.clientSettings(ClientSettings.builder()
						.requireAuthorizationConsent(true)
						.jwkSetUrl("http://127.0.0.1:7000/jwkSet")
						.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256)
						.build())
				.build();

		// 初始化客户端
		this.save(registeredClient);
		this.save(privateKeyJwtClient);
		this.save(privateKeyJwtClient1);
		this.save(deviceClient);
		this.save(pkceClient);
		this.save(opaqueClient);
	}
}